//-------------------------------------------------------------------
//	Copyright (c) 2013  Zhirnov Andrey
//	This file is part of the "Noise Library" project.
//	See copyright notice in "linNoiseCL.h".
//-------------------------------------------------------------------

#pragma once

#include <Includes.h>

#if !defined(_NOISE_CROSS_PLATFORM) and !defined(_NOISE_NO_IMAGE_VIEWER)

namespace Noise
{

	//
	// Image Viewer
	//

	class CImageViewer : public IWindowUpdater
	{
	// types
	private:
		struct Message
		{
			enum EType
			{
				UNKNOWN	= 0,
				SET_GL_TEXTURE,
				SET_IMAGE_DATA,
			};

			struct GLTextureID
			{
				GLuint			texId;
			};

			struct ImageData
			{
				void *			data;
				uint			width;
				uint			height;
				EPixelFormat	format;
				EPixelType		type;
			};

			EType	type;
			bool	result;
			union {
				GLTextureID	tex;
				ImageData	img;
			};

			Message (EType type) : type(type), result(false) { ZeroMem( this ); }
		};


	// variables
	private:
		GLWindow		_window;
		TThread			_imageViewerThread;
		GLuint			_vao;
		GLuint			_vbo;
		GLuint			_programMono,
						_programColored,
						_programChannels;
		GLuint			_texture;
		bool			_canDeleteTexture;
		TEventSync		_onMessageProcessed;
		Message			_msg;
		atomic_bool		_isNewMessage;
		int				_outputMode;


	// methods
	public:
		CImageViewer () :
			_vao(0), _vbo(0), _programMono(0), _programColored(0), _programChannels(0),
			_texture(0), _canDeleteTexture(false), _msg(Message::UNKNOWN), _outputMode(RENDER_COLOR_TEXTURE)
		{}

		~CImageViewer ()
		{}

		bool Create (void *sharedRC = null)
		{
			CWindow::TDescriptor	s_desc;
			GLWindow::TGLDescriptor	s_gl_desc;

			s_desc.vSize			= uvec2( 640, 480 );
			s_desc.vPos				= ivec2( 0, 0 );
			s_desc.eStyle			= e_wnd_style::CENTERED;
			s_desc.uFreq			= 0;
			s_desc.bFullScreen		= false;

			s_gl_desc.eFlags		= e_glinit_flags::DEFAULT_CORE;
			s_gl_desc.hSharedRC		= (winapi::HGLRC) sharedRC;
			s_gl_desc.uColorBits	= 32;
			s_gl_desc.uDepthBits	= 24;
			s_gl_desc.uStencilBits	= 0;
			s_gl_desc.uMSAASamples	= 0;
			s_gl_desc.uColorSamples	= 0;
			s_gl_desc.bVSync		= true;

			_window.AddUpdater( this );
			_window.AllowPause( false );

			CHECK_RETURN( _window.CreateGLInThread( &_imageViewerThread,
													GLWindow::TDescriptorExt( s_desc, s_gl_desc, true ),
													"libNoiseCL Image Viewer" ) );
			return true;
		}

		void Destroy ()
		{
			_window.Destroy();
			_imageViewerThread.Wait();
		}

		void SetSize (const uvec2 &size)
		{
			CWindow::TDescriptor	s_desc = _window.Descriptor();

			s_desc.vSize = size;
			_window.ChangeParams( s_desc, false );
		}

		void SetFlags (EImageViewerFlags flags)
		{
			const int outputFlagsMask = RENDER_MONO_TEXTURE | RENDER_COLOR_TEXTURE | RENDER_TEXTURE_PER_CHANNEL;

			_outputMode = flags & outputFlagsMask;
		}

		const uvec2 & GetSize () const
		{
			return _window.Size();
		}

		void DrawTexture (GLuint tex)
		{
			_msg = Message( Message::SET_GL_TEXTURE );
			_msg.tex.texId = tex;

			_isNewMessage = true;

			_onMessageProcessed.Wait();
			_isNewMessage = false;
		}

		void DrawImage (const uvec2 &dim, EPixelFormat format, EPixelType type, void *pixels)
		{
			_msg = Message( Message::SET_IMAGE_DATA );
			_msg.img.data	= pixels;
			_msg.img.format	= format;
			_msg.img.type	= type;
			_msg.img.width	= dim.x;
			_msg.img.height	= dim.y;

			_isNewMessage = true;

			_onMessageProcessed.Wait();
			_isNewMessage = false;
		}


	private:
		void _CreateQuad ()
		{
			if ( _vao != 0 and _vbo != 0 and _programMono != 0 and
				 _programColored != 0 and _programChannels != 0 )
				return;

			const float	vertices[] = {
				-1.0f,  1.0f,
				-1.0f, -1.0f,
				 1.0f,  1.0f,
				 1.0f, -1.0f
			};

			const char *	vertexShader = 
				"#version 150 core\n"
				"in  vec2 inPosition;\n"
				"out vec2 vTexcoord;\n"
				"void main() {\n"
				"	gl_Position = vec4(inPosition, 0.0, 1.0);\n"
				"	vTexcoord = inPosition * 0.5 + 0.5;\n"
				"}";

			const char *	fragmentShaderMono =
				"#version 150 core\n"
				"out vec4 outColor;\n"
				"in  vec2 vTexcoord;\n"
				"uniform sampler2D unImage;\n"
				"void main() {\n"
				"	outColor = vec4(vec3(texture(unImage, vTexcoord).r), 1.0);\n"
				"}";

			const char *	fragmentShaderColored = 
				"#version 150 core\n"
				"out vec4 outColor;\n"
				"in  vec2 vTexcoord;\n"
				"uniform sampler2D unImage;\n"
				"void main() {\n"
				"	outColor = texture(unImage, vTexcoord);\n"
				"}";

			const char *	fragmentShaderChannels = 
				"#version 150 core\n"
				"out vec4 outColor;\n"
				"in  vec2 vTexcoord;\n"
				"uniform sampler2D unImage;\n"
				"void main() {\n"
				"	vec2  tc      = vTexcoord * 2.0;\n"
				"	ivec2 coord   = ivec2(floor(tc));\n"
				"	int   channel = (coord.x + coord.y) & 3;\n"
				"	float color   = texture(unImage, tc - vec2(coord))[channel];\n"
				"	outColor = vec4(0.0);\n"
				"	if (channel == 0)	outColor.r = color;	else\n"
				"	if (channel == 1)	outColor.g = color;	else\n"
				"	if (channel == 2)	outColor.b = color;	else\n"
				"						outColor.rgb = vec3(color);\n"
				"}";

			GLuint	vs = 0,
					fsMono = 0,
					fsColored = 0,
					fsChannels = 0;
			GLint	compile_status = 0;

			//char	buf[1024];

			GL_CALL( glGenVertexArrays( 1, &_vao ) );
			GL_CALL( glGenBuffers( 1, &_vbo ) );
			GL_CALL( vs = glCreateShader( GL_VERTEX_SHADER ) );
			GL_CALL( fsMono = glCreateShader( GL_FRAGMENT_SHADER ) );
			GL_CALL( fsColored = glCreateShader( GL_FRAGMENT_SHADER ) );
			GL_CALL( fsChannels = glCreateShader( GL_FRAGMENT_SHADER ) );

			GL_CALL( glBindVertexArray( _vao ) );
			GL_CALL( glBindBuffer( GL_ARRAY_BUFFER, _vbo ) );
			GL_CALL( glBufferData( GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW ) );
			GL_CALL( glEnableVertexAttribArray( 0 ) );
			GL_CALL( glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 0, null ) );
			GL_CALL( glBindVertexArray( 0 ) );

			GL_CALL( glShaderSource( vs, 1, (const GLchar **) &vertexShader, null ) );
			GL_CALL( glShaderSource( fsMono, 1, (const GLchar **) &fragmentShaderMono, null ) );
			GL_CALL( glShaderSource( fsColored, 1, (const GLchar **) &fragmentShaderColored, null ) );
			GL_CALL( glShaderSource( fsChannels, 1, (const GLchar **) &fragmentShaderChannels, null ) );
			GL_CALL( glCompileShader( vs ) );
			GL_CALL( glCompileShader( fsMono ) );
			GL_CALL( glCompileShader( fsColored ) );
			GL_CALL( glCompileShader( fsChannels ) );

			GL_CALL( glGetShaderiv( vs, GL_COMPILE_STATUS,  &compile_status ) );
			//GL_CALL( glGetShaderInfoLog( vs, count_of(buf), null, buf ) );
			CHECK_VALUE_R( compile_status == GL_TRUE, );
			
			GL_CALL( glGetShaderiv( fsMono, GL_COMPILE_STATUS,  &compile_status ) );
			//GL_CALL( glGetShaderInfoLog( fsMono, count_of(buf), null, buf ) );
			CHECK_VALUE_R( compile_status == GL_TRUE, );
			
			GL_CALL( glGetShaderiv( fsColored, GL_COMPILE_STATUS,  &compile_status ) );
			//GL_CALL( glGetShaderInfoLog( fsColored, count_of(buf), null, buf ) );
			CHECK_VALUE_R( compile_status == GL_TRUE, );
			
			GL_CALL( glGetShaderiv( fsChannels, GL_COMPILE_STATUS,  &compile_status ) );
			//GL_CALL( glGetShaderInfoLog( fsChannels, count_of(buf), null, buf ) );
			CHECK_VALUE_R( compile_status == GL_TRUE, );

			GL_CALL( _programMono = glCreateProgram() );
			GL_CALL( glAttachShader( _programMono, vs ) );
			GL_CALL( glAttachShader( _programMono, fsMono ) );
			GL_CALL( glBindAttribLocation( _programMono, 0, "inPosition" ) );
			GL_CALL( glLinkProgram( _programMono ) );
			GL_CALL( glGetProgramiv( _programMono, GL_LINK_STATUS, &compile_status ) );
			//GL_CALL( glGetProgramInfoLog( _programMono, count_of(buf), null, buf ) );
			CHECK_VALUE_R( compile_status == GL_TRUE, );

			GL_CALL( _programColored = glCreateProgram() );
			GL_CALL( glAttachShader( _programColored, vs ) );
			GL_CALL( glAttachShader( _programColored, fsColored ) );
			GL_CALL( glBindAttribLocation( _programColored, 0, "inPosition" ) );
			GL_CALL( glLinkProgram( _programColored ) );
			GL_CALL( glGetProgramiv( _programColored, GL_LINK_STATUS, &compile_status ) );
			//GL_CALL( glGetProgramInfoLog( _programColored, count_of(buf), null, buf ) );
			CHECK_VALUE_R( compile_status == GL_TRUE, );

			GL_CALL( _programChannels = glCreateProgram() );
			GL_CALL( glAttachShader( _programChannels, vs ) );
			GL_CALL( glAttachShader( _programChannels, fsChannels ) );
			GL_CALL( glBindAttribLocation( _programChannels, 0, "inPosition" ) );
			GL_CALL( glLinkProgram( _programChannels ) );
			GL_CALL( glGetProgramiv( _programChannels, GL_LINK_STATUS, &compile_status ) );
			//GL_CALL( glGetProgramInfoLog( _programChannels, count_of(buf), null, buf ) );
			CHECK_VALUE_R( compile_status == GL_TRUE, );

			GL_CALL( glDeleteShader( vs ) );
			GL_CALL( glDeleteShader( fsMono ) );
			GL_CALL( glDeleteShader( fsColored ) );
			GL_CALL( glDeleteShader( fsChannels ) );

			GL_CALL( glClearColor( 0.0f, 0.0f, 0.0f, 1.0f ) );
			GL_CALL( glDisable( GL_CULL_FACE ) );
			GL_CALL( glDisable( GL_DEPTH_TEST ) );
			GL_CALL( glDisable( GL_BLEND ) );
		}
		
		void _Draw ()
		{
			uvec2 const & size = _window.Size();

			GL_CALL( glViewport( 0, 0, size.x, size.y ) );
			GL_CALL( glClear( GL_COLOR_BUFFER_BIT ) );
			
			switch ( _outputMode )
			{
				case RENDER_MONO_TEXTURE :
					GL_CALL( glUseProgram( _programMono ) );
					break;

				case RENDER_COLOR_TEXTURE :
					GL_CALL( glUseProgram( _programColored ) );
					break;

				case RENDER_TEXTURE_PER_CHANNEL :
					GL_CALL( glUseProgram( _programChannels ) );
					break;
			}

			GL_CALL( glBindTexture( GL_TEXTURE_2D, _texture ) );
			GL_CALL( glBindVertexArray( _vao ) );
			GL_CALL( glDrawArrays( GL_TRIANGLE_STRIP, 0, 4 ) );

			_window.SwapBuffers();
		}

		static bool _GetGLFormat (EPixelFormat format, EPixelType type, GLenum &ifmt, GLenum &fmt, GLenum &gltype)
		{
			struct GLFormat {
				EPixelFormat	pformat;
				EPixelType		ptype;
				GLenum			ifmt,
								fmt,
								type;
			};
			static const GLFormat	formats[] = {
				{ R,	SINT8,		GL_R8I,		GL_RED_INTEGER,		GL_BYTE },
				{ RG,	SINT8,		GL_RG8I,	GL_RG_INTEGER,		GL_BYTE },
				{ RGB,	SINT8,		GL_RGB8I,	GL_RGB_INTEGER,		GL_BYTE },
				{ RGBA,	SINT8,		GL_RGBA8I,	GL_RGBA_INTEGER,	GL_BYTE },
				{ R,	UINT8,		GL_R8UI,	GL_RED_INTEGER,		GL_UNSIGNED_BYTE },
				{ RG,	UINT8,		GL_RG8UI,	GL_RG_INTEGER,		GL_UNSIGNED_BYTE },
				{ RGB,	UINT8,		GL_RGB8UI,	GL_RGB_INTEGER,		GL_UNSIGNED_BYTE },
				{ RGBA,	UINT8,		GL_RGBA8UI,	GL_RGBA_INTEGER,	GL_UNSIGNED_BYTE },
				{ R,	FLOAT16,	GL_R16F,	GL_RED,				GL_HALF_FLOAT },
				{ RG,	FLOAT16,	GL_RG16F,	GL_RG,				GL_HALF_FLOAT },
				{ RGB,	FLOAT16,	GL_RGB16F,	GL_RGB,				GL_HALF_FLOAT },
				{ RGBA,	FLOAT16,	GL_RGBA16F,	GL_RGBA,			GL_HALF_FLOAT },
				{ R,	FLOAT32,	GL_R32F,	GL_RED,				GL_FLOAT },
				{ RG,	FLOAT32,	GL_RG32F,	GL_RG,				GL_FLOAT },
				{ RGB,	FLOAT32,	GL_RGB32F,	GL_RGB,				GL_FLOAT },
				{ RGBA,	FLOAT32,	GL_RGBA32F,	GL_RGBA,			GL_FLOAT },
			};

			for (uint i = 0; i < count_of(formats); ++i)
			{
				if ( formats[i].pformat == format and formats[i].ptype == type )
				{
					ifmt	= formats[i].ifmt;
					fmt		= formats[i].fmt;
					gltype	= formats[i].type;
					return true;
				}
			}

			return false;
		}

		void _ProcessMessages ()
		{
			if ( not _isNewMessage )
				return;

			switch ( _msg.type )
			{
				case Message::SET_GL_TEXTURE :
					if ( _canDeleteTexture and _texture != 0 ) {
						GL_CALL( glDeleteTextures( 1, &_texture ) );
						_texture = 0;
					}

					_texture = _msg.tex.texId;
					_canDeleteTexture = false;
					break;

				case Message::SET_IMAGE_DATA : {
					GLenum ifmt = 0;
					GLenum fmt  = 0;
					GLenum type = 0;

					_msg.result = _GetGLFormat( _msg.img.format, _msg.img.type, ifmt, fmt, type );
					if ( not _msg.result )
						break;
					
					if ( _canDeleteTexture and _texture != 0 ) {
						// reuse texture
					}
					else {
						// create new texture
						_canDeleteTexture = true;
						GL_CALL( glGenTextures( 1, &_texture ) );
					}

					GL_CALL( glBindTexture( GL_TEXTURE_2D, _texture ) );
					GL_CALL( glTexImage2D( GL_TEXTURE_2D, 0, ifmt, _msg.img.width, _msg.img.height, 0, fmt, type, _msg.img.data ) );
					GL_CALL( glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ) );
					GL_CALL( glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ) );
					GL_CALL( glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ) );
					GL_CALL( glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ) );
					GL_CALL( glBindTexture( GL_TEXTURE_2D, 0 ) );
					break;
				}
			}

			_msg.result = true;

			_onMessageProcessed.Signal();
			_window.Show();
		}

		// IWindowUpdater //
		virtual void OnUpdate (CWindow *wnd) override
		{
			_CreateQuad();
			_ProcessMessages();

			//if ( _texture == 0 )
			//	_window.Hide();

			_Draw();
		}

	};

}	// Noise

#endif	// not _NOISE_CROSS_PLATFORM